/*
 * Copyright 2015 泛泛o0之辈
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.jfast.framework.schedule.analyzer;

import cn.jfast.framework.base.prop.CoreConsts;
import cn.jfast.framework.log.LogFactory;
import cn.jfast.framework.log.LogType;
import cn.jfast.framework.log.Logger;
import cn.jfast.framework.schedule.ScheduleCronException;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.StringTokenizer;

/**
 * Cron表达式解析
 */
public class CronAnalyzer {
    private Logger log = LogFactory.getLogger(LogType.JFast, CronAnalyzer.class);
    private Calendar currCalendar;
    private Calendar nextCalendar;
    private String date;
    private String cronExpression;
    private String task;
    private TimeWrapper currSecond;
    private TimeWrapper currMinute;
    private TimeWrapper currHour;
    private TimeWrapper currDay;
    private TimeWrapper currMonth;
    private TimeWrapper currYear;
    private int nextSecond;
    private int nextMinute;
    private int nextHour;
    private int nextDay;
    private int nextMonth;
    private int nextYear;
    private long nowTimeInMill;
    private long nextTimeInMill;
    private SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public CronAnalyzer(String cronExpression, String task) {
        this.task = task;
        this.cronExpression = cronExpression;
        StringTokenizer strToken = new StringTokenizer(cronExpression, " ", false);
        currSecond = timeWrapper(strToken.nextToken().trim(), TimeType.SECOND);
        currMinute = timeWrapper(strToken.nextToken().trim(), TimeType.MINUTE);
        currHour = timeWrapper(strToken.nextToken().trim(), TimeType.HOUR);
        currDay = timeWrapper(strToken.nextToken().trim(), TimeType.DAY);
        currMonth = timeWrapper(strToken.nextToken().trim(), TimeType.MONTH);
        currYear = timeWrapper(strToken.nextToken().trim(), TimeType.YEAR);
        currCalendar = Calendar.getInstance();
        currCalendar.setTime(new Date());
        nowTimeInMill = currCalendar.getTimeInMillis();
        nextCalendar = Calendar.getInstance();
    }

    private TimeWrapper timeWrapper(String time, TimeType timeType) {
        if (time.equals("*"))
            return new TimeWrapper(time, CronTime.ASTERISK, timeType, this);
        else if (time.matches("\\d+\\-\\d+"))
            return new TimeWrapper(time, CronTime.HYPHEN, timeType, this);
        else if (time.matches("\\d+,\\d+"))
            return new TimeWrapper(time, CronTime.COMMA, timeType, this);
        else if (time.matches("\\d+"))
            return new TimeWrapper(time, CronTime.NUMBER, timeType, this);
        else if (time.matches("((,|\\-)?\\d+(,|\\-)?)*"))
            return new TimeWrapper(time, CronTime.HYPHEN_AND_COMMA, timeType, this);
        else
            throw new ScheduleCronException("CRON 无法解析: %s, 目标表达式: %s "
                    , time, cronExpression);
    }

    public long getExecuteTime() {
        nextYear = currYear.getExecuteTime();
        if (currMonth.getCronType() == CronTime.ASTERISK) {
            if (nextYear == currCalendar.get(Calendar.YEAR))
                nextMonth = currMonth.getExecuteTime();
            else
                nextMonth = currMonth.getDefaultTime();
        } else {
            nextMonth = currMonth.getExecuteTime();
        }
        if (currDay.getCronType() == CronTime.ASTERISK) {
            if (nextMonth == currCalendar.get(Calendar.MONTH) + 1)
                nextDay = currDay.getExecuteTime();
            else
                nextDay = currDay.getDefaultTime();
        } else {
            nextDay = currDay.getExecuteTime();
        }
        if (currHour.getCronType() == CronTime.ASTERISK) {
            if (nextDay == currCalendar.get(Calendar.DAY_OF_MONTH))
                nextHour = currHour.getExecuteTime();
            else
                nextHour = currHour.getDefaultTime();
        } else {
            nextHour = currHour.getExecuteTime();
        }
        if (currMinute.getCronType() == CronTime.ASTERISK) {
            if (nextHour == currCalendar.get(Calendar.HOUR_OF_DAY))
                nextMinute = currMinute.getExecuteTime();
            else
                nextMinute = currMinute.getDefaultTime();
        } else {
            nextMinute = currMinute.getExecuteTime();
        }
        if (currSecond.getCronType() == CronTime.ASTERISK) {
            if (nextMinute == currCalendar.get(Calendar.MINUTE))
                nextSecond = currSecond.getExecuteTime();
            else
                nextSecond = currSecond.getDefaultTime();
        } else {
            nextSecond = currSecond.getExecuteTime();
        }
        if (!nextTimeAvailable()) {
            nextSecond();
        }
        if(CoreConsts.develop_mode)
            log.info("定时任务[ %s ]预载;执行时间 [ %s ]."
                    , task, fmt.format(nextCalendar.getTime()));
        return nextTimeInMill - nowTimeInMill;
    }

    private void nextSecond() {
        nextSecond = currSecond.getExecuteTime();
        if (!nextTimeAvailable()) {
            nextMinute();
        }
    }

    private void nextMinute() {
        nextSecond = currSecond.getDefaultTime();
        nextMinute = currMinute.getExecuteTime();
        if (!nextTimeAvailable()) {
            nextHour();
        }
    }

    private void nextHour() {
        nextMinute = currMinute.getDefaultTime();
        nextHour = currHour.getExecuteTime();
        if (!nextTimeAvailable()) {
            nextDay();
        }
    }

    private void nextDay() {
        nextHour = currHour.getDefaultTime();
        nextDay = currDay.getExecuteTime();
        if (!nextTimeAvailable()) {
            nextMonth();
        }
    }

    private void nextMonth() {
        nextDay = currDay.getDefaultTime();
        nextMonth = currMonth.getExecuteTime();
        if (!nextTimeAvailable()) {
            nextYear();
        }
    }

    private void nextYear() {
        nextMonth = currMonth.getDefaultTime();
        nextYear = currYear.getExecuteTime();
        nextTimeAvailable();
    }

    private boolean nextTimeAvailable() {
        date = nextYear + "-" + nextMonth + "-" + nextDay + " " + nextHour + ":" + nextMinute + ":" + nextSecond;
        try {
            nextCalendar.setTime(fmt.parse(date));
        } catch (ParseException e) {
        	log.error("", e);
        }
        nextTimeInMill = nextCalendar.getTimeInMillis();
        return nextTimeInMill > nowTimeInMill;
    }

    public String getCronExpression() {
        return cronExpression;
    }

    public String getTask() {
        return task;
    }

}
